package pt.fct.mpc.rfbul.misc;

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import pt.fct.mpc.rfbul.database.MapperDatabaseAdapter;
import pt.fct.mpc.rfbul.database.MapperDatabaseEntry;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.wifi.ScanResult;
import android.util.Log;

public class PositionHandling {
	private static final String TAG = "RFBUL Position Handling";
	private Context context;

	public PositionHandling(Context context) {
		this.context = context;
	}

	public MapperDatabaseEntry doFindPosition(Set<PositionDevice> devices) {
		return doFindPosition(devices, null);
	}

	public MapperDatabaseEntry doFindPosition(Set<PositionDevice> devices,
			String exclude_criterion) {

		MapperDatabaseEntry best_match = null;

		MapperDatabaseAdapter db = MapperDatabaseAdapter.getInstance(context);

		SQLiteDatabase conn = db.open();
		// conn.beginTransaction();

		String query_rooms = "select distinct "
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_ROOM + " from "
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP;

		Cursor cursor_rooms = conn.rawQuery(query_rooms, null);
		cursor_rooms.moveToFirst();

		List<String> rooms_list = new LinkedList<String>();
		for (int i = 0; i < cursor_rooms.getCount(); i++) {
			int col_idx_room = cursor_rooms
					.getColumnIndexOrThrow(MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_ROOM);

			String room = cursor_rooms.getString(col_idx_room);
			rooms_list.add(room);
			cursor_rooms.moveToNext();
		}

		cursor_rooms.close();
		conn.close();

		for (String r : rooms_list) {
			MapperDatabaseEntry match = doFindBestMatchByRoom(r, devices,
					exclude_criterion);

			if (match == null)
				continue;

			if (best_match == null)
				best_match = match;
			else if (match.value < best_match.value)
				best_match = match;
		}
		
		String absolute_pos = getAbsolutePositionByMatch(best_match);
		
		if (best_match != null)
			best_match.ref_pos = absolute_pos;

		// conn.setTransactionSuccessful();

		// Log.d(TAG, "Best Match for room = " + r + " is null!");
		return best_match;
	}

	private MapperDatabaseEntry doFindBestMatchByRoom(String r,
			Set<PositionDevice> devices, String exclude_criterion) {
		assert ((r != null) && (devices != null));

		String query = "select "
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_ROOM + ","
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_REL_POS + ","
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_MAC + ","
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_SIGNAL 
				+" from "
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP;

		String query_where = " where "
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_ROOM + "='" + r
				+ "'";

		if (exclude_criterion != null) {
			String[] exclude_str = exclude_criterion.split(":");
			if (exclude_str.length < 2) {
				Log.e(TAG, "Something terrible happend in here!");
				return null;
			}

			String exclude_room = exclude_str[0];
			String exclude_pos = exclude_str[1];

			if (r.equalsIgnoreCase(exclude_room)) {
				query_where += " and "
						+ MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_REL_POS
						+ "!='" + exclude_pos + "'";
			}
		}

		query += query_where + " order by "
				+ MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_REL_POS;

		MapperDatabaseAdapter db = MapperDatabaseAdapter.getInstance(context);
		SQLiteDatabase conn = db.open();
		Cursor cursor_room_positions = conn.rawQuery(query, null);
		cursor_room_positions.moveToFirst();

		HashMap<String, List<PositionDevice>> room_map = new HashMap<String, List<PositionDevice>>();

		int col_idx_room = cursor_room_positions
				.getColumnIndexOrThrow(MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_ROOM);
		int col_idx_relpos = cursor_room_positions
				.getColumnIndexOrThrow(MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_REL_POS);
		int col_idx_mac = cursor_room_positions
				.getColumnIndexOrThrow(MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_MAC);
		int col_idx_signal = cursor_room_positions
				.getColumnIndexOrThrow(MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_SIGNAL);

		for (int i = 0; i < cursor_room_positions.getCount(); i++) {
			String room = cursor_room_positions.getString(col_idx_room);
			String relpos = cursor_room_positions.getString(col_idx_relpos);
			String mac = cursor_room_positions.getString(col_idx_mac);
			int signal = cursor_room_positions.getInt(col_idx_signal);

			PositionDevice dev = new PositionDevice(mac, "", signal);

			List<PositionDevice> position_device_lst = room_map.get(relpos);
			if (position_device_lst == null) {
				position_device_lst = new LinkedList<PositionDevice>();
				room_map.put(relpos, position_device_lst);
			}
			position_device_lst.add(dev);

			cursor_room_positions.moveToNext();
		}

		cursor_room_positions.close();
		conn.close();

		// Find best match between all entries on room_map.

		Log.d(TAG, "Finding best match for room '" + r + "'");
		MapperDatabaseEntry result = doGetRoomBestMatch(devices, room_map);

		if (result != null)
			result.room = r;
		return result;
	}

	private MapperDatabaseEntry doGetRoomBestMatch(Set<PositionDevice> devices,
			HashMap<String, List<PositionDevice>> room_map) {
		Set<String> positions = room_map.keySet();

		MapperDatabaseEntry result = null;
		MapperDatabaseEntry temp;

		int bestDif = 1000;
		PositionDevice bestDevice = null;

		for (String p : positions) {
			temp = new MapperDatabaseEntry();
			temp.rel_pos = p;

			List<PositionDevice> pos_dev_lst = room_map.get(p);
			for (PositionDevice dev : devices) {
				boolean found = false;
				for (int i = 0; i < pos_dev_lst.size();) {
					PositionDevice lst_dev = pos_dev_lst.get(i);

					if (lst_dev.BSSID.equalsIgnoreCase(dev.BSSID) == false) {
						i++;
						continue;
					}
					found = true;
					pos_dev_lst.remove(i);

					int dif = Math.max(Math.abs(lst_dev.signal), Math
							.abs(dev.signal))
							- Math.min(Math.abs(lst_dev.signal), Math
									.abs(dev.signal));

					if (dif < bestDif) {
						bestDif = dif;
						bestDevice = lst_dev;
					}
				}
				if (found) {
					bestDif = 1000;
					temp.devices.put(bestDevice.BSSID, bestDevice);
				}
			}

			while (!pos_dev_lst.isEmpty()) {
				PositionDevice dev = pos_dev_lst.remove(0);
				for (int i = 0; i < pos_dev_lst.size();) {
					PositionDevice lst_dev = pos_dev_lst.get(i);

					if (lst_dev.BSSID.equalsIgnoreCase(dev.BSSID) == false) {
						i++;
						continue;
					}
					pos_dev_lst.remove(i);

					if (lst_dev.signal < dev.signal) {
						dev = lst_dev;
					}
				}
				temp.devices.put(dev.BSSID, dev);
			}

			doCalculateMetricEuclidean(temp, devices);
			if (result == null || temp.value < result.value)
				result = temp;
		}
		return result;
	}

	/**
	 * Do a simple position search using simply those devices which are 
	 * detected, and retrieving only those entries in the database containing
	 * all of them. This method is not ideal, since it ignores any device which
	 * may be detected in the limit of its range and could be useful to 
	 * calculate the distances.
	 *  
	 * @param devices
	 * @param exclude_criterion You should not use this.
	 * @return
	 */
	public MapperDatabaseEntry doFindPositionSimple(Set<PositionDevice> devices,
			String exclude_criterion) 
	{
		MapperDatabaseAdapter db_adapter = MapperDatabaseAdapter
				.getInstance(context);

		// Set<String> mac_set = new HashSet<String>();
		// List<ScanResult> results_list = new LinkedList<ScanResult>();

		// for (PositionDevice r : devices)
		// {
		// if (r.SSID.equalsIgnoreCase(MapperConstants.SSID_TO_LOOK_FOR))
		// {
		// mac_set.add(r.BSSID);
		// results_list.add(r);
		// }
		// }

		Log.d(TAG, "exclude criterion = " + exclude_criterion);

		Map<String, List<MapperDatabaseEntry>> matches = db_adapter
				.getEntriesByMAC(devices, exclude_criterion);
		// db_adapter.getEntriesByMAC(mac_set);

		Collection<List<MapperDatabaseEntry>> matches_values = matches.values();

		List<MapperDatabaseEntry> best_matches = new LinkedList<MapperDatabaseEntry>();

		for (List<MapperDatabaseEntry> lst : matches_values) {
			MapperDatabaseEntry e = doGetRoomBestMatchSimple(lst, devices);
			best_matches.add(e);
		}

		MapperDatabaseEntry best_match = null;
		double best_match_signal = Integer.MAX_VALUE;

		for (MapperDatabaseEntry e : best_matches) {
			double metric = doCalculateMetric(e, devices);
			if (metric < best_match_signal) {
				best_match = e;
				best_match_signal = metric;
			}
		}

		if (best_match != null)
			Log.d(TAG, "Best Matching Entry: room = " + best_match.room);

		return best_match;
	}

	/**
	 * Used by doFindPositionSimple.
	 * 
	 * @param lst
	 * @param devices
	 * @return
	 */
	private MapperDatabaseEntry doGetRoomBestMatchSimple(
			List<MapperDatabaseEntry> lst, Set<PositionDevice> devices) {
		MapperDatabaseEntry best_match_pos = null;
		double best_match_pos_signal = Integer.MAX_VALUE;

		for (MapperDatabaseEntry e : lst) {
			double metric = doCalculateMetric(e, devices);

			if (metric < best_match_pos_signal) {
				best_match_pos = e;
				best_match_pos_signal = metric;
			}
		}
		return best_match_pos;
	}

	private String getAbsolutePositionByMatch(MapperDatabaseEntry entry)
	{
		String abs_pos = "";
		if (entry == null)
			return abs_pos;
		
		String room = entry.room;
		String rel_pos = entry.rel_pos;
		
		String query =
			"select "+MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_REF_POS
			+" from "+MapperDatabaseAdapter.MAPPER_TABLE_MAP
			+" where "
			+MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_ROOM + "='"+room+"'"
			+" and "
			+MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_REL_POS+"='"+rel_pos+"'"
			+" limit 1";
		
		MapperDatabaseAdapter db = MapperDatabaseAdapter.getInstance(context);
		SQLiteDatabase conn = db.open();
		
		Cursor cursor = conn.rawQuery(query, null);
		cursor.moveToFirst();

		if (cursor.getCount() > 0)
		{
			int col_ref_pos_idx = 
				cursor.getColumnIndexOrThrow(
					MapperDatabaseAdapter.MAPPER_TABLE_MAP_COL_REF_POS);
			
			abs_pos = cursor.getString(col_ref_pos_idx);
		}
		
		cursor.close();
		conn.close();
		
		return abs_pos;
	}

	public double doCalculateMetric(MapperDatabaseEntry e,
			Set<PositionDevice> devices) {
		// return doCalculateMetricSimple(e, results);
		return doCalculateMetricEuclidean(e, devices);
	}

	public double doCalculateMetricSimple(MapperDatabaseEntry e,
			List<ScanResult> results) {
		assert (e.signal_values.length == results.size());

		int sum = 0;
		for (ScanResult r : results) {
			String result_mac = r.BSSID;

			for (int i = 0; i < e.mac_values.length; i++) {
				String entry_mac = e.mac_values[i];
				if (entry_mac.equalsIgnoreCase(result_mac)) {
					int sig_entry = Math.abs(e.signal_values[i]);
					int sig_result = Math.abs(r.level);

					int sig_max = Math.max(sig_entry, sig_result);
					int sig_min = Math.min(sig_entry, sig_result);

					int sig_dif = sig_max - sig_min;
					sum += sig_dif;
					break;
				}
			}
		}

		return (double) sum;
	}

	public double doCalculateMetricEuclidean(MapperDatabaseEntry e,
			Set<PositionDevice> devices) {

		int tmp_result = 0;
		List<String> ssdibs = new Vector<String>();
		for (PositionDevice r : devices) {
			String result_mac = r.BSSID;
			ssdibs.add(result_mac);

			for (int i = 0; i < e.devices.size(); i++) {
				if (e.devices.get(result_mac) != null) {
					int dif = e.devices.get(result_mac).signal - r.signal;
					int value = dif * dif;
					tmp_result += value;
				} else {
					int dif = r.signal + 100;
					int value = dif * dif;
					tmp_result += value;
				}
			}
		}
		Set<String> keys = e.devices.keySet();
		Iterator<String> it = keys.iterator();
		while (it.hasNext()) {
			String ssd = it.next();
			if (!ssdibs.contains(ssd)) {
				ssdibs.add(ssd);
				int dif = e.devices.get(ssd).signal + 100;
				int value = dif * dif;
				tmp_result += value;
			}
		}

		double result = Math.sqrt((double) tmp_result);
		e.value = result;
		return result;
	}
}
